前言
继续前面两篇 react 的分析。提到 react 16,除了 fiber 机制之外,还有其调度机制。这里就不得不提 requestIdleCallback 了,react 采用了 requestIdleCallback 的思想来实现调度,为什么说思想呢,因为 requestIdleCallback 是新出的 api,兼容性差,很多现代浏览器都不支持。于是 react 团队就写了一个 ployfill。
requestIdleCallback
关于 requestIdleCallback,请先看看 MDN 上面的介绍。requestIdleCallback 的出现意使得浏览器可以在空闲的时候执行 callback,这对单线程的 V8 而言就相当又用了。假设你需要大量涉及到 DOM 的操作的计算,在运算时,浏览器可能就会出现明显的卡顿行为,甚至不能进行任何操作,因为是单线程,就算用
在输入处理,给定帧渲染和合成之后,用户的主线程就会变得空闲,直到下一帧的开始,或者有优先的待处理任务,或者是存在输入。用户所看到的页面都是一帧一帧渲染出来的,下图中可以看到一帧的过程:
虽然上面是介绍 requestAnimationFrame 时用到的图片,但是也很详细的介绍到一帧里面浏览器都做了些什么,一帧里面除了上面干的活外就是空余时间 idle 了。可以看看 W3C requestidlecallback 的介绍:
Layout 和 paint 就是我们常见的重排和重绘,也就是 render 的部分,而 Task 就包含了各种任务,包括输入处理,js 处理等等。完成这些事情还有多少时间剩余呢?一般是按照 60fps 来处理的。至于为什么是 60fps 呢,大多数浏览器遵循 W3C 所建议的刷新频率也就是 60fps 了,requestAnimationFrame 里面就是按照频率来的。60 fps 就已经能够保证看到的动画不会一卡一卡了。所以在一帧 16.66ms 的时间里面空闲的时间就是 requestIdleCallback 调用处理的阶段了。
requestIdleCallback 参数
MDN 里面介绍到语法,这里再提一下:
var handle = window.requestIdleCallback(callback[, options]);
callback 是要执行的回调函数,会传入 deadline 对象作为参数,deadline 包含:
- timeRemaining:剩余时间,单位 ms,指的是该帧剩余时间。
- didTimeout:布尔型,true 表示该帧里面没有执行回调,超时了。
options 里面有个重要参数 timeout,如果给定 timeout,那到了时间,不管有没有剩余时间,都会立刻执行回调 callback。
React 中的实现
先看两张 amazing 的图: 首先是 React 16 之前版本的
在之前的版本里面,若 React 要开始更新的时候,就会处于深度调用的状态,程序会一直处理更新,而不会接受处理外部的输入。如果更新的层级多而深则会导致更新时间长的问题。到了 React 16 fiber 的阶段呢,如下所示;
可以明显的看到每次处理完一部分之后,react 都会从非常深的调用栈上看看有没有其他优先要做的事情,有则开始做其他事情,如输入事件等等,结束后再回过头来继续之前的事情。是不是很神奇!这种时间丝滑般的设计,对于大量数据的渲染很有帮助。看看 react 中设计的 requestIdleCallback ployfill。
const localRequestAnimationFrame = requestAnimationFrame;
// 链表头部与尾部
let headOfPendingCallbacksLinkedList = null;
let tailOfPendingCallbacksLinkedList = null;
// frameDeadlineObject 为传入callback的参数 deadline
const frameDeadlineObject = {
didTimeout: false,
timeRemaining() {
// 通过 frameDeadline 来判断,该帧剩余时间
const remaining = frameDeadline - now();
return remaining > 0 ? remaining : 0;
},
};
// export 对外函数,也就是 requestIdleCallback ployfill
scheduleWork = function(callback, options) {
const timeoutTime = now() + options.timeout;
const scheduledCallbackConfig: CallbackConfigType = {
scheduledCallback: callback,
timeoutTime,
prev: null,
next: null,
};
// 省略将scheduledCallbackConfig插入到链表里面过程
if (!isAnimationFrameScheduled) {
isAnimationFrameScheduled = true;
localRequestAnimationFrame(animationTick);
}
}
// requestAnimationFrame 调用函数
const animationTick = function(rafTime) {
isAnimationFrameScheduled = false;
// 更新 frameDeadline
frameDeadline = rafTime + activeFrameTime;
if (!isIdleScheduled) {
isIdleScheduled = true;
window.postMessage(messageKey, '*');
}
}
// 省略消息监听处理部分
// 执行 callback,与传参 deadline
const callUnsafely = function(callbackConfig, arg) {
const callback = callbackConfig.scheduledCallback;
callback(arg);
// 总是会删除调用过的 callbackConfig
cancelScheduledWork(callbackConfig);
}
cancelScheduledWork = function(callbackConfig) {
// 在链表中删除对应节点,并维护好pre以及next关系
}
可以看出这里是采用 requestAnimationFrame 来代替 requestIdleCallback,这也很好理解。关键地方在于 deadline 参数的传递。这里用了 frameDeadlineObject 来表示,每次 requestAnimationFrame 的时候都会更新 frameDeadlineObject 对象里面的 frameDeadline 基线。frameDeadline 正如其名,就是每次 requestAnimationFrame 开始的时间以及该帧时长之和。只要 now()
的时间大于它,自然是表示没有空闲时间。
比如在一个帧里面,可能第一个 callback 运行时间过长,frameDeadline - now()
不为正数,则不会无法继续执行。程序会在下一帧开始的时候执行 input 事件什么的,若有空闲时间才执行 idle callback。
上文中通过链表的结构,每次都将传入的 callback 和 timeoutTime 保存起来,以 pre/next 的形式来维系。并在 callUnsafely 里面调用完之后就删除掉。回顾一下上面处理流程,通过 scheduleWork 传入 callback,用 requestAnimationFrame 方式第一次启用 animationTick,并用事件的方式 window.postMessage
消息的方式来调用。其中省略的消息监听的处理如下:
// messageKey 为特点字符串
const idleTick = function(event) {
if (event.source !== window || event.data !== messageKey) {
return;
}
isIdleScheduled = false;
callTimedOutCallbacks();
let currentTime = now();
// 空闲时间判断
while (
frameDeadline - currentTime > 0 &&
headOfPendingCallbacksLinkedList !== null
) {
const latestCallbackConfig = headOfPendingCallbacksLinkedList;
frameDeadlineObject.didTimeout = false;
callUnsafely(latestCallbackConfig, frameDeadlineObject);
currentTime = now();
}
// 继续下一个节点,调用requestAnimationFrame
if (
!isAnimationFrameScheduled &&
headOfPendingCallbacksLinkedList !== null
) {
isAnimationFrameScheduled = true;
localRequestAnimationFrame(animationTick);
}
}
window.addEventListener('message', idleTick, false);
// 如果设置了 timeoutTime 的话,自然是无脑执行到底的,而不会把时间让渡予下一帧
const callTimedOutCallbacks = function() {
const currentTime = now();
const timedOutCallbacks = [];
let currentCallbackConfig = headOfPendingCallbacksLinkedList;
while (currentCallbackConfig !== null) {
if (timeoutTime !== -1 && timeoutTime <= currentTime) {
timedOutCallbacks.push(currentCallbackConfig);
}
}
// 存在 timeoutTime 的事件,并且发生超时了,那就执行,不考虑帧的问题了
if (timedOutCallbacks.length > 0) {
frameDeadlineObject.didTimeout = true;
for (let i = 0, len = timedOutCallbacks.length; i < len; i++) {
callUnsafely(timedOutCallbacks[i], frameDeadlineObject);
}
}
}
animationTick 结合 idleTick 形成消息传递事件的发送方和接收方,同时也分别是 requestAnimationFrame 回调函数和触发函数。通过 messageKey 来识别是否是通知的自己。idleTick 里面的循环判断和 timeRemaining 相同,判断是否有空闲时间,有才进行 callUnsafely,执行 callback。
fiber 与 requestIdleCallback
在上面的代码好像都没有看到 timeRemaining 使用的地方哦,其实在 workLoop 里面才会有判断
function workLoop(isYieldy) {
if(isYield) {
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
function shouldYield() {
if (deadline === null || deadlineDidExpire) {
return false;
}
if (deadline.timeRemaining() > 1) {
return false;
}
deadlineDidExpire = true;
return true;
}
对于异步更新的每次执行 performUnitOfWork 前都会判断一次是否有空余时间,有才会继续。通过这个地方判断是否有空闲时间。
前者 idleTick 里面是在初始化的时候判断,能否立马执行 performWork 函数,以及在该帧里面能够执行链表的下一个 performWork。 后则 workLoop 是在 render/reconcilation 阶段的 workLoop 循环里面判断空闲时间,有就继续。当然在第二个阶段 commit 是没有检查空闲时间过程的。从而实现了之前版本没有现实的方式。当一次组件更新时间较长的时候,仍然运行 input 等操作,同时,更重要的是不会发生局部更新。事件能打断的只是 render/reconcilation 阶段,这个阶段不会发生任何的真实 DOM 的变化。这也是调度里面最神奇的地方,空闲时间的检查仅仅发生在 render/reconcilation 阶段。
只是该阶段还是会执行 ComponentWillUpdate 这些生命钩子,well,react 团队表示着没有关系。
于是到了timeRemaining之后,render/reconcilation 阶段就会被打断,继续处理浏览器的其他输入事件。并在输入后执行
expirationTime 与优先级别
之前我们计算 expirationTime,都是按照同步来算的,也就是值为 1(SYNC)。既然是同步自然就不需要调度来控制任务系统了。当 expirationTime !== SYNC
的时候,才进入 requestIdleCallback 的任务调度
function requestWork(root, expirationTime) {
if (expirationTime === Sync) {
performSyncWork();
} else {
// 进入调度
scheduleCallbackWithExpirationTime(expirationTime);
}
}
这里的 expirationTime 是 root.expirationTime
。也就是说 React 当前是处于同步还是调度模式,是由 root 的 expirationTime 决定的。这也就是说明了其模式分两种,一种是同步,一种是调度。而调度的优先级将取决于 expirationTime。
##总结 本文更多的只是结合 requestIdleCallback 来介绍异步相关过程,但是更多的内容还是没有介绍到,将放在下篇文章 react 开启异步渲染与优先级 介绍到。